<?php

namespace api\components;

use Closure;
use \Redis;
use Yii;
use yii\db\Exception;
use yii\helpers\Json;
use yii\redis\Connection;

class RedisCache
{

    const CACHE_DEFALUT_TIMEOUT = 60 * 60;
    const CACHE_NAMESPACE = 'QRCODE:';
    const CACHE_PREFIX = '';

    protected $redis;

    static private $instance;

    public function __construct()
    {
        $this->redis = \Yii::$app->redis;
    }

    private function __clone()
    {
    }

    static public function instance()
    {
        if (!self::$instance instanceof static) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    public function sadd($key, $values, $exprie = -1)
    {
        $result = $this->redis->sadd($this->generateKey($key), $values);
        if ($exprie > 0) {
            $this->redis->ttl($key, $exprie);
        }
        return $result;
    }

    public function smembers($key)
    {
        return $this->redis->smembers($this->generateKey($key));
    }

    private function getRedisClient()
    {
        return Yii::$app->redis;
    }

    private function generateKey($key)
    {
        return self::CACHE_NAMESPACE . self::CACHE_PREFIX . strval($key);
    }

    public function ttl($key)
    {
        return $this->redis->ttl($this->generateKey($key));
    }

    /**
     * @param mixed $value
     * @return string
     */
    private function serializeValue($value)
    {
        $type = gettype($value);
        switch ($type) {
            case 'integer':
            case 'string':
            case 'boolean':
            case 'NULL':
                break;
            case 'array':
            case 'object':
            default:
                $value = serialize($value);
                break;
        }
        return Json::encode(['type' => $type, 'value' => $value]);
    }

    /**
     * @param string $value
     * @return mixed
     */
    private function unSerializeValue($value)
    {
        if ($value === false) return null;
        if (is_null($value)) return null;
        $value = Json::decode($value);
        switch ($value['type']) {
            case 'integer':
            case 'string':
            case 'boolean':
            case 'NULL':
                $res = $value['value'];
                break;
            case 'array':
            case 'object':
            default:
                $res = unserialize($value['value']);
                break;
        }
        return $res;
    }

    /**
     * 存入缓存,若 ttl<=0 则无过期时间（不推荐）
     * demo:
     * RedisCache::instance()->set( 'key' , 'value' , 'ttl' );
     * @param $key
     * @param $value
     * @param float|int $ttl
     * @return bool
     */
    public function set($key, $value, $ttl = self::CACHE_DEFALUT_TIMEOUT): bool
    {
        try {
            $command_params = [$this->generateKey($key), $this->serializeValue($value)];
            $ttl > 0 && $command_params = array_merge($command_params, ['EX', $ttl]);
            return $this->redis->executeCommand('SET', $command_params);
        } catch (Exception $e) {
            \Yii::warning(['msg' => 'redis 错误', 'executeCommand' => 'setnx', 'Exception' => Json::encode($e)]);
            return false;
        }
    }

    /**
     * 存入缓存,若 ttl<=0 则无过期时间（不推荐）
     * 若key已存在，则返回false
     * demo:
     * RedisCache::instance()->setnx( 'key' , 'value' , 'ttl' );
     * @param string $key
     * @param $value
     * @param float|int $ttl
     * @return bool
     */
    public function setnx(string $key, $value, int $ttl = self::CACHE_DEFALUT_TIMEOUT)
    {
        try {
            $command_params = [$this->generateKey($key), $this->serializeValue($value), 'NX'];
            $ttl > 0 && $command_params = array_merge($command_params, ['EX', $ttl]);
            return $this->redis->executeCommand('SET', $command_params);
        } catch (Exception $e) {
            \Yii::warning(['msg' => 'redis 错误', 'executeCommand' => 'setnx', 'Exception' => Json::encode($e)]);
            return false;
        }
    }

    /**
     * 获取缓存内容，若没有缓存或缓存内容为null，则返回default的值 （default也可以传入匿名函数）
     * demo:
     * RedisCache::instance()->get( 'key' );
     * or
     * RedisCache::instance()->get( 'key' , 'default' );
     * @param $key
     * @param null|mixed|Closure $default
     * @return mixed|null
     */
    public function get($key, $default = null)
    {
        $key = $this->generateKey($key);
        return $this->unSerializeValue($this->redis->get($key)) ?? ($default instanceof Closure ? $default() : $default);
    }

    /**
     * 新内容替换原值，并返回原值
     * demo:
     * RedisCache::instance()->getset( 'key' , 'value' , 'ttl' );
     * @param string $key
     * @param mixed $value
     * @param float|int $ttl
     * @return mixed|null
     */
    public function getset(string $key, $value, int $ttl = self::CACHE_DEFALUT_TIMEOUT)
    {
        $key = $this->generateKey($key);
        $value = $this->unSerializeValue($this->redis->getSet(
            $key,
            $this->serializeValue($value)
        ));
        $ttl > 0 && $this->redis->expire($key, $ttl);
        return $value;
    }

    /**
     * 批量删除多个key
     * demo:
     * RedisCache::instance()->deleteMultiple( ['key1','key2','key3'] );
     * @param array $key
     * @return int|mixed
     */
    public function deleteMultiple($key)
    {
        $del_key = [];
        $del_num = 0;
        foreach ($key as $item) {
            is_array($item)
                ? $del_num += $this->deleteMultiple($item)
                : $del_key[] = $this->generateKey(strval($item));
        }
        return $del_num + $this->redis->del(...$del_key);
    }

    /**
     * 删除缓存
     * demo:
     * RedisCache::instance()->delete( 'key1' );
     * @param $key
     * @return mixed
     */
    public function delete($key)
    {
        return $this->redis->del($this->generateKey($key));
    }

    /**
     * 根据字符串匹配key并删除
     * demo:
     * RedisCache::instance()->setMultiple(
     *    [ 'user_1' => 'value1' , 'user_2' => 'value2' , 'user_3' => 'value3' , 'u_123' => 'value4'  ]
     * );
     * RedisCache::instance()->deleteByPattern( 'user*' );
     * RedisCache::instance()->get( 'user_1' ); //null
     * RedisCache::instance()->get( 'user_2' ); //null
     * RedisCache::instance()->get( 'user_3' ); //null
     * RedisCache::instance()->get( 'u_123' );  //value4
     * @param string $pattern
     * @return int
     */
    public function deleteByPattern(string $pattern): int
    {
        $total = 0;
        $pattern = $this->generateKey($pattern);
        foreach (array_chunk($this->keys($pattern), 400) as $item) {
            $total += $this->getRedisClient()->del(...$item);
        }
        return $total;
    }

    public function clear(): int
    {
        return $this->deleteByPattern('*');
    }

    /**
     * 批量获取缓存内容(此功能需要原生php-redis扩展支持)
     * demo:
     * RedisCache::instance()->setMultiple(
     *    [ 'key1' => 'value1' , 'key2' => 'value2' , 'key3' => 'value3'  ]
     * );
     *
     * // [ 'key1' => 'value1' , 'key2' => 'value2' , 'key3' => 'value3'  ]
     * RedisCache::instance()->getMultiple( [ 'key1' , 'key2' , 'key3' ] );
     *
     * @param $keys
     * @param null $default
     * @return array
     */
    public function getMultiple($keys, $default = null)
    {
        $result = [];
        $redis = $this->getRedisClient()->multi(Redis::PIPELINE);
        foreach ($keys as $item) {
            $redis->get($this->generateKey($item));
        }
        foreach ($redis->exec() as $n => $item) {
            $result[strval($keys[$n])] = $this->unSerializeValue($item) ?? $default;
        }
        return $result;
    }

    /**
     * 批量设置缓存内容(此功能需要原生php-redis扩展支持)
     * demo:
     * RedisCache::instance()->setMultiple(
     *    [ 'key1' => 'value1' , 'key2' => 'value2' , 'key3' => 'value3'  ]
     * );
     *
     * // [ 'key1' => 'value1' , 'key2' => 'value2' , 'key3' => 'value3'  ]
     * RedisCache::instance()->getMultiple( [ 'key1' , 'key2' , 'key3' ] );
     *
     * @param $values
     * @param float|int $ttl
     * @return array|void
     */
    public function setMultiple($values, $ttl = self::CACHE_DEFALUT_TIMEOUT)
    {
        $redis = $this->getRedisClient()->multi(Redis::PIPELINE);
        foreach ($values as $key => $value) {
            $redis->set($this->generateKey($key), $this->serializeValue($value), $ttl);
        }
        return $redis->exec();
    }

    /**
     * 检测是否有key
     * RedisCache::instance()->setMultiple(
     *    [ 'key1' => 'value1' , 'key2' => 'value2'  ]
     * );
     *
     * RedisCache::instance()->has('key1'); //'1'
     * RedisCache::instance()->has('key2'); //'1'
     * RedisCache::instance()->has('key3'); //'0'
     *
     * @param $key
     * @return mixed
     */
    public function has($key)
    {
        return $this->redis->exists($this->generateKey($key));
    }

    /**
     * 重命名缓存key
     * RedisCache::instance()->set('srcKey','value');
     * RedisCache::instance()->rename('srcKey','dstKey');
     * RedisCache::instance()->get('srcKey'); //null
     * RedisCache::instance()->get('dstKey'); //'value'
     *
     * force:强制更名,若更换后的key已经存在，则强制更名，更换后的key将被替换
     * RedisCache::instance()->setMultiple(
     *    [ 'srcKey' => 'value1' , 'dstKey' => 'value2'  ]
     * );
     * RedisCache::instance()->rename('srcKey','dstKey'); //false
     * RedisCache::instance()->get('srcKey'); //value1
     * RedisCache::instance()->get('dstKey'); //value2
     * RedisCache::instance()->rename('srcKey','dstKey',true); //true
     * RedisCache::instance()->get('srcKey'); //null
     * RedisCache::instance()->get('dstKey'); //value1
     *
     * @param string $srcKey
     * @param string $dstKey
     * @param bool $force
     * @return bool
     */
    public function rename(string $srcKey, string $dstKey, $force = false): bool
    {
        return $force
            ? $this->redis->rename($this->generateKey($srcKey), $this->generateKey($dstKey))
            : $this->redis->renameNx($this->generateKey($srcKey), $this->generateKey($dstKey));
    }

    /**
     * 匹配缓存key
     * RedisCache::instance()->setMultiple(
     *    [ 'user_1' => 'value1' , 'user_2' => 'value2' , 'user_3' => 'value3' , 'u_123' => 'value4'  ]
     * );
     * RedisCache::instance()->keys('user_*'); // ['user_1','user_2','user_3']
     * @param string $pattern
     * @return array
     */
    public function keys(string $pattern): array
    {
        return $this->getRedisClient()->keys($pattern);
    }
}
